package middleware

import (
    "bytes"
    "fmt"
    "github.com/gin-gonic/gin"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/httputil"
    "os"
    "runtime"
    "server/app"
    "strings"
    "time"
)

const reset = "\033[0m"

var (
    dunno     = []byte("???")
    centerDot = []byte("·")
    dot       = []byte(".")
    slash     = []byte("/")
)

// 捕获应用程序错误
func RecoveryMiddleware(skipper ...SkipperFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        if len(skipper) > 0 && skipper[0](c) {
            c.Next()
            return
        }
        logger := app.GetLogger()
        defer func() {
            if err := recover(); err != nil {
                // 检查连接是否断开
                var brokenPipe bool
                if ne, ok := err.(*net.OpError); ok {
                    if se, ok := ne.Err.(*os.SyscallError); ok {
                        if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
                            brokenPipe = true
                        }
                    }
                }
                if logger != nil {
                    stack := stack(3)
                    httpRequest, _ := httputil.DumpRequest(c.Request, false)
                    headers := strings.Split(string(httpRequest), "\r\n")
                    for idx, header := range headers {
                        current := strings.Split(header, ":")
                        if current[0] == "Authorization" {
                            headers[idx] = current[0] + ": *"
                        }
                    }
                    if brokenPipe {
                        logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
                    } else if gin.IsDebugging() {
                        logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
                            timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
                    } else {
                        logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
                            timeFormat(time.Now()), err, stack, reset)
                    }
                }

                // 如果连接断开，则无法向其写入状态。
                if brokenPipe {
                    c.Error(err.(error))
                    c.Abort()
                } else {
                    c.JSON(http.StatusOK, app.NewRest().Code(500).Msg(err.(error).Error()))
                    c.Writer.WriteHeaderNow()
                    c.Abort()
                }
            }
        }()
        c.Next()
    }
}

func timeFormat(t time.Time) string {
    var timeString = t.Format("2006/01/02 - 15:04:05")
    return timeString
}

// stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte {
    buf := new(bytes.Buffer) // the returned data
    // As we loop, we open files and read them. These variables record the currently
    // loaded file.
    var lines [][]byte
    var lastFile string
    for i := skip; ; i++ { // Skip the expected number of frames
        pc, file, line, ok := runtime.Caller(i)
        if !ok {
            break
        }
        // Print this much at least.  If we can't find the source, it won't show.
        fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
        if file != lastFile {
            data, err := ioutil.ReadFile(file)
            if err != nil {
                continue
            }
            lines = bytes.Split(data, []byte{'\n'})
            lastFile = file
        }
        fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
    }
    return buf.Bytes()
}

// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
    fn := runtime.FuncForPC(pc)
    if fn == nil {
        return dunno
    }
    name := []byte(fn.Name())
    // The name includes the path name to the package, which is unnecessary
    // since the file name is already included.  Plus, it has center dots.
    // That is, we see
    //	runtime/debug.*T·ptrmethod
    // and want
    //	*T.ptrmethod
    // Also the package path might contains dot (e.g. code.google.com/...),
    // so first eliminate the path prefix
    if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
        name = name[lastSlash+1:]
    }
    if period := bytes.Index(name, dot); period >= 0 {
        name = name[period+1:]
    }
    name = bytes.Replace(name, centerDot, dot, -1)
    return name
}

// source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte {
    n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
    if n < 0 || n >= len(lines) {
        return dunno
    }
    return bytes.TrimSpace(lines[n])
}
